在前幾篇,讀者已經用 Mongory 的 matcher tree 跑過一輪
今天把重點拉回到「日常可用的 API 與整合體驗」:Query builder 鏈式介面、Converters(資料/鍵/值/條件),以及如何在 Rails 中一鍵完成初始化與客製化
require 'mongory'
Mongory.enable_symbol_snippets!
Mongory.register(Array)
# 模擬資料
records = [
{ 'name' => 'Ann', 'age' => 19, 'status' => 'active', 'tags' => [{ 'name' => 'ruby', 'priority' => 6 }] },
{ 'name' => 'Ben', 'age' => 22, 'status' => 'pending', 'tags' => [{ 'name' => 'rails', 'priority' => 3 }] },
{ 'name' => 'Cody', 'age' => 17, 'status' => 'active', 'tags' => [{ 'name' => 'ruby', 'priority' => 8 }] }
]
# 建構 query
q = records.mongory
.where(:age.gte => 18)
.any_of({ :status => 'active' }, { :name.regex => /^A/ })
.in(:status => %w[active pending])
q.explain
q.each { |r| p r['name'] }
where
:加入條件any_of
:語義為 $or
(會包在 $and
架構內,維持可預期的邏輯合併)in
/nin
:集合條件糖衣(等價於把值包成 $in/$nin
)limit(n)
:立即生效、縮小後續運算集合Converters 讓 Mongory 在面對不同資料型別、鍵表達方式與條件來源時仍能夠表現一致
Mongory 預設提供四個面向:
"a.b" → {"a" => {"b" => ...}}
這些 converter 會在 matcher tree 建置或比對過程中被正確調用,讓條件與資料都落在「可比較」的標準層
以 ActiveRecord 舉例:
Mongory.register(ActiveRecord::Relation)
users = User.where(active: true) # 走資料庫索引
# 接著用 Mongory 做應用層二次過濾(非索引欄位或複雜陣列條件)
q = users.mongory
.where(:last_login.gte => 7.days.ago)
.where(:tags.elem_match => { :name => 'ruby', :priority.gt => 5 })
q.each { |u| p u.id }
Mongoid 已有整合模組(載入 mongory/mongoid
),會註冊必要的轉換,讓 Criteria
走到 Mongory 時鍵/值/資料都能順利比對
rails g mongory:install
會產生 config/initializers/mongory.rb
,內容如下:
Mongory.configure do |mc|
# 啟用 symbol 運算子語法糖
mc.enable_symbol_snippets!
# 註冊常見集合類別(可在 Array、ActiveRecord::Relation 等等直接呼叫 .mongory)
mc.register(Array)
mc.register(ActiveRecord::Relation) # 如果使用 Active record
mc.register(Mongoid::Criteria) # 如果使用 Mongoid
mc.register(Sequel::Dataset) # 如果使用 Sequal
# 這裡配置 Converters(依專案需求增補)
mc.data_converter.configure do |dc|
dc.register(ActiveRecord::Base, :attributes) # 使用 Active record
dc.register(Mongoid::Document, :as_document) # 使用 Mongoid
dc.register(BSON::ObjectId, :to_s) # 使用 Mongoid
dc.register(Sequel::Model) { values.transform_keys(&:to_s) } # 使用 Sequal
end
mc.condition_converter.configure do |cc|
# 條件轉換器底下分兩種,分別對應鍵轉換與值轉換
cc.key_converter.configure do |kc|
# 鍵轉換需要你提供能夠接收一個參數的 method 或 block
# Example:
# kc.register(MyKeyObject, :trans_to_string_key_pair)
# kc.register(MyEnumKey, ->(val) { { "my_enum" => val.to_s } })
end
cc.value_converter.configure do |vc|
# Example:
# vc.register(MyCollectionType) { map { |v| vc.convert(v) } }
# vc.register(MyWrapperType) { unwrap_and_return_value }
vc.register(BSON::ObjectId, :to_s) # 使用 Mongoid
end
end
end
# 如果你的專案不用 Rails ,無法配合 rails g 的指令,那這邊可以直接 copy 拿去用 :p
rails g mongory:matcher by_proc
會建立對應檔案與註冊片段(也可自動更新 initializer)
示例:
class ByProcMatcher < Mongory::Matchers::AbstractMatcher
def match(data)
@condition.call(data)
end
def check_validity!
return if @condition.is_a?(Proc)
raise TypeError, "$byProc needs a proc."
end
end
Mongory::Matchers.register(:by_proc, '$byProc', ByProcMatcher)
# 如果有啟用語法糖,之後的 query 就可以直接使用 Symbol#by_proc
records.mongory.where(:name.by_proc => method(:name_valid?).to_proc)
symbol snippets
或是建立自訂 matcher 前先確定命名不會覆蓋到既有方法
class UsersController < ApplicationController
def index
scope = User.where(active: true)
filtered = scope.mongory
.where(:age.gte => 18)
.any_of({ :status => 'active' }, { :tags.elem_match => { :name => 'ruby' } })
.limit(50)
render json: filtered.map { |u| { id: u.id, name: u.name } }
end
end
下一篇將回到觀測性:explain
與 trace
的心法、輸出閱讀,以及如何用它們縮短除錯時間